Skip to content

Conversation

medismailben
Copy link
Member

@medismailben medismailben commented Oct 3, 2025

This patch introduces a new scripting affordance: ScriptedFrameProvider.

This allows users to provide custom stack frames for real native threads,
augmenting or replacing the standard unwinding mechanism. This is useful
for:

  • Providing frames for custom calling conventions or languages
  • Reconstructing missing frames from crash dumps or core files
  • Adding diagnostic or synthetic frames for debugging

The frame provider supports four merge strategies:

  • Replace: Replace entire stack with scripted frames
  • Prepend: Add scripted frames before real stack
  • Append: Add scripted frames after real stack
  • ReplaceByIndex: Replace specific frames by index

With this change, frames can be synthesized from different sources:

  • Either from a dictionary containing a PC address and frame index
  • Or by creating a ScriptedFrame python object for full control

Architecture changes:

  • Moved ScriptedFrame from Plugins to Interpreter to avoid
    layering violations
  • Moved RegisterContextMemory from Plugins to Target as it only
    depends on Target and Utility layers
  • Added ScriptedFrameProvider C++ wrapper and Python interface
  • Updated Thread::GetStackFrameList to apply merge strategies

rdar://161834688

Signed-off-by: Med Ismail Bennani [email protected]

@llvmbot llvmbot added the lldb label Oct 3, 2025
@medismailben medismailben changed the title [lldb] Introduce ScritedFrameProvider [lldb] Introduce ScriptedFrameProvider Oct 3, 2025
@medismailben medismailben requested a review from jimingham October 3, 2025 15:57
@llvmbot
Copy link
Member

llvmbot commented Oct 3, 2025

@llvm/pr-subscribers-lldb

Author: Med Ismail Bennani (medismailben)

Changes

Patch is 27.35 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/161870.diff

19 Files Affected:

  • (modified) lldb/include/lldb/API/SBThread.h (+2)
  • (added) lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h (+27)
  • (modified) lldb/include/lldb/Interpreter/ScriptInterpreter.h (+5)
  • (added) lldb/include/lldb/Interpreter/ScriptedFrameProvider.h (+51)
  • (modified) lldb/include/lldb/Target/Thread.h (+9)
  • (modified) lldb/include/lldb/lldb-forward.h (+6)
  • (modified) lldb/source/API/SBThread.cpp (+27)
  • (modified) lldb/source/Commands/CommandObjectFrame.cpp (+95)
  • (modified) lldb/source/Commands/CommandObjectThread.cpp (+1)
  • (modified) lldb/source/Interpreter/CMakeLists.txt (+1)
  • (added) lldb/source/Interpreter/ScriptedFrameProvider.cpp (+86)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt (+1)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h (+1)
  • (added) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp (+57)
  • (added) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.h (+41)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h (+4)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp (+5)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h (+3)
  • (modified) lldb/source/Target/Thread.cpp (+30)
diff --git a/lldb/include/lldb/API/SBThread.h b/lldb/include/lldb/API/SBThread.h
index e9fe5858d125e..7b91228528bc7 100644
--- a/lldb/include/lldb/API/SBThread.h
+++ b/lldb/include/lldb/API/SBThread.h
@@ -229,6 +229,8 @@ class LLDB_API SBThread {
 
   SBValue GetSiginfo();
 
+  void RegisterFrameProvider(const char *class_name, SBStructuredData &args_data);
+
 private:
   friend class SBBreakpoint;
   friend class SBBreakpointLocation;
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
new file mode 100644
index 0000000000000..7618d5e15d563
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
@@ -0,0 +1,27 @@
+//===-- ScriptedFrameProviderInterface.h ------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
+
+#include "lldb/lldb-private.h"
+
+#include "ScriptedInterface.h"
+
+namespace lldb_private {
+class ScriptedFrameProviderInterface : public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, lldb::ThreadSP thread_sp,
+                     StructuredData::DictionarySP args_sp) = 0;
+
+  virtual StructuredData::ArraySP GetStackFrames() { return {}; }
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 024bbc90a9a39..76ade002089bb 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -27,6 +27,7 @@
 #include "lldb/Host/StreamFile.h"
 #include "lldb/Interpreter/Interfaces/OperatingSystemInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedFrameInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedPlatformInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
@@ -536,6 +537,10 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedFrameProviderInterfaceSP CreateScriptedFrameProviderInterface() {
+    return {};
+  }
+
   virtual lldb::ScriptedThreadPlanInterfaceSP
   CreateScriptedThreadPlanInterface() {
     return {};
diff --git a/lldb/include/lldb/Interpreter/ScriptedFrameProvider.h b/lldb/include/lldb/Interpreter/ScriptedFrameProvider.h
new file mode 100644
index 0000000000000..6c4053f11eeb3
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/ScriptedFrameProvider.h
@@ -0,0 +1,51 @@
+//===-- ScriptedFrameProvider.h --------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_INTERPRETER_SCRIPTEDFRAMEPROVIDER_H
+#define LLDB_INTERPRETER_SCRIPTEDFRAMEPROVIDER_H
+
+#include "lldb/Utility/ScriptedMetadata.h"
+#include "lldb/Utility/Status.h"
+#include "lldb/lldb-forward.h"
+#include "llvm/Support/Error.h"
+
+namespace lldb_private {
+
+class ScriptedFrameProvider {
+public:
+  /// Constructor that initializes the scripted frame provider.
+  ///
+  /// \param[in] thread_sp
+  ///     The thread for which to provide scripted frames.
+  ///
+  /// \param[in] scripted_metadata
+  ///     The metadata containing the class name and arguments for the
+  ///     scripted frame provider.
+  ///
+  /// \param[out] error
+  ///     Status object to report any errors during initialization.
+  ScriptedFrameProvider(lldb::ThreadSP thread_sp,
+                        const ScriptedMetadata &scripted_metadata,
+                        Status &error);
+  ~ScriptedFrameProvider();
+
+  /// Get the stack frames from the scripted frame provider.
+  ///
+  /// \return
+  ///     An Expected containing the StackFrameListSP if successful,
+  ///     otherwise an error describing what went wrong.
+  llvm::Expected<lldb::StackFrameListSP> GetStackFrames();
+
+private:
+  lldb::ThreadSP m_thread_sp;
+  lldb::ScriptedFrameProviderInterfaceSP m_interface_sp;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_SCRIPTEDFRAMEPROVIDER_H
diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h
index 688c056da2633..d0c33f557b12b 100644
--- a/lldb/include/lldb/Target/Thread.h
+++ b/lldb/include/lldb/Target/Thread.h
@@ -1294,6 +1294,10 @@ class Thread : public std::enable_shared_from_this<Thread>,
   ///     The PC value before execution was resumed.  May not be available;
   ///     an empty std::optional is returned in that case.
   std::optional<lldb::addr_t> GetPreviousFrameZeroPC();
+                 
+   void SetScriptedFrameProvider(const ScriptedMetadata &scripted_metadata);
+
+   void ClearScriptedFrameProvider();
 
 protected:
   friend class ThreadPlan;
@@ -1338,6 +1342,8 @@ class Thread : public std::enable_shared_from_this<Thread>,
 
   lldb::StackFrameListSP GetStackFrameList();
 
+  llvm::Expected<lldb::StackFrameListSP> GetScriptedFrameList();
+
   void SetTemporaryResumeState(lldb::StateType new_state) {
     m_temporary_resume_state = new_state;
   }
@@ -1400,6 +1406,9 @@ class Thread : public std::enable_shared_from_this<Thread>,
   /// The Thread backed by this thread, if any.
   lldb::ThreadWP m_backed_thread;
 
+  /// The Scripted Frame Provider, if any.
+  lldb::ScriptedFrameProviderSP m_frame_provider_sp;
+
 private:
   bool m_extended_info_fetched; // Have we tried to retrieve the m_extended_info
                                 // for this thread?
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index af5656b3dcad1..85045a803b07a 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -188,6 +188,8 @@ class Scalar;
 class ScriptInterpreter;
 class ScriptInterpreterLocker;
 class ScriptedFrameInterface;
+class ScriptedFrameProvider;
+class ScriptedFrameProviderInterface;
 class ScriptedMetadata;
 class ScriptedBreakpointInterface;
 class ScriptedPlatformInterface;
@@ -411,6 +413,10 @@ typedef std::shared_ptr<lldb_private::ScriptSummaryFormat>
 typedef std::shared_ptr<lldb_private::ScriptInterpreter> ScriptInterpreterSP;
 typedef std::shared_ptr<lldb_private::ScriptedFrameInterface>
     ScriptedFrameInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedFrameProvider>
+    ScriptedFrameProviderSP;
+typedef std::shared_ptr<lldb_private::ScriptedFrameProviderInterface>
+    ScriptedFrameProviderInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedMetadata> ScriptedMetadataSP;
 typedef std::unique_ptr<lldb_private::ScriptedPlatformInterface>
     ScriptedPlatformInterfaceUP;
diff --git a/lldb/source/API/SBThread.cpp b/lldb/source/API/SBThread.cpp
index 4e4aa48bc9a2e..a18d540f2a017 100644
--- a/lldb/source/API/SBThread.cpp
+++ b/lldb/source/API/SBThread.cpp
@@ -39,6 +39,7 @@
 #include "lldb/Target/ThreadPlanStepOut.h"
 #include "lldb/Target/ThreadPlanStepRange.h"
 #include "lldb/Utility/Instrumentation.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/Utility/State.h"
 #include "lldb/Utility/Stream.h"
 #include "lldb/Utility/StructuredData.h"
@@ -1324,3 +1325,29 @@ SBValue SBThread::GetSiginfo() {
     return SBValue();
   return thread_sp->GetSiginfoValue();
 }
+
+void SBThread::RegisterFrameProvider(const char *class_name,
+                                     SBStructuredData &dict) {
+  LLDB_INSTRUMENT_VA(this, class_name, args_data);
+
+  ThreadSP thread_sp = m_opaque_sp->GetThreadSP();
+  if (!thread_sp)
+    return;
+  
+  if (!dict.IsValid() || !dict.m_impl_up)
+    return;
+
+  StructuredData::ObjectSP obj_sp = dict.m_impl_up->GetObjectSP();
+
+  if (!obj_sp)
+    return;
+
+  StructuredData::DictionarySP dict_sp =
+      std::make_shared<StructuredData::Dictionary>(obj_sp);
+  if (!dict_sp || dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
+    return;
+
+
+  ScriptedMetadata metadata(class_name, dict_sp);
+  thread_sp->SetScriptedFrameProvider(metadata);
+}
diff --git a/lldb/source/Commands/CommandObjectFrame.cpp b/lldb/source/Commands/CommandObjectFrame.cpp
index 88a02dce35b9d..02d62aa9249d1 100644
--- a/lldb/source/Commands/CommandObjectFrame.cpp
+++ b/lldb/source/Commands/CommandObjectFrame.cpp
@@ -16,6 +16,7 @@
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Interpreter/OptionArgParser.h"
 #include "lldb/Interpreter/OptionGroupFormat.h"
+#include "lldb/Interpreter/OptionGroupPythonClassWithDict.h"
 #include "lldb/Interpreter/OptionGroupValueObjectDisplay.h"
 #include "lldb/Interpreter/OptionGroupVariable.h"
 #include "lldb/Interpreter/Options.h"
@@ -29,6 +30,7 @@
 #include "lldb/Target/Target.h"
 #include "lldb/Target/Thread.h"
 #include "lldb/Utility/Args.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/ValueObject/ValueObject.h"
 
 #include <memory>
@@ -1223,6 +1225,97 @@ class CommandObjectFrameRecognizer : public CommandObjectMultiword {
   ~CommandObjectFrameRecognizer() override = default;
 };
 
+#pragma mark CommandObjectFrameProvider
+
+#define LLDB_OPTIONS_frame_provider_register
+#include "CommandOptions.inc"
+
+class CommandObjectFrameProviderRegister : public CommandObjectParsed {
+public:
+  CommandObjectFrameProviderRegister(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "frame provider register",
+            "Register frame provider into current thread.",
+            nullptr, eCommandRequiresThread),
+
+        m_class_options("frame provider", true, 'C', 'k', 'v', 0) {
+    m_all_options.Append(&m_class_options, LLDB_OPT_SET_1 | LLDB_OPT_SET_2,
+                         LLDB_OPT_SET_ALL);
+    m_all_options.Finalize();
+
+    AddSimpleArgumentList(eArgTypeRunArgs, eArgRepeatOptional);
+  }
+
+  ~CommandObjectFrameProviderRegister() override = default;
+
+  Options *GetOptions() override { return &m_all_options; }
+
+  std::optional<std::string> GetRepeatCommand(Args &current_command_args,
+                                              uint32_t index) override {
+    // No repeat for "process launch"...
+    return std::string("");
+  }
+
+protected:
+  void DoExecute(Args &launch_args, CommandReturnObject &result) override {
+    ScriptedMetadata metadata(m_class_options.GetName(), m_class_options.GetStructuredData());
+
+    Thread *thread = m_exe_ctx.GetThreadPtr();
+    if (!thread) {
+      result.AppendError("invalid thread");
+      return;
+    }
+
+    thread->SetScriptedFrameProvider(metadata);
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+    result.AppendMessageWithFormat(
+        "Successfully registered scripted frame provider '%s'\n",
+        m_class_options.GetName().c_str());
+  }
+
+  OptionGroupPythonClassWithDict m_class_options;
+  OptionGroupOptions m_all_options;
+};
+
+class CommandObjectFrameProviderClear : public CommandObjectParsed {
+public:
+  CommandObjectFrameProviderClear(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "frame provider clear",
+                            "Delete registered frame provider.", nullptr) {}
+
+  ~CommandObjectFrameProviderClear() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Thread *thread = m_exe_ctx.GetThreadPtr();
+    if (!thread) {
+      result.AppendError("invalid thread");
+      return;
+    }
+    
+    thread->ClearScriptedFrameProvider();
+    
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
+class CommandObjectFrameProvider : public CommandObjectMultiword {
+public:
+  CommandObjectFrameProvider(CommandInterpreter &interpreter)
+      : CommandObjectMultiword(
+            interpreter, "frame provider",
+            "Commands for registering and viewing frame providers.",
+            "frame provider [<sub-command-options>] ") {
+    LoadSubCommand("register", CommandObjectSP(new CommandObjectFrameProviderRegister(
+                              interpreter)));
+    LoadSubCommand(
+        "clear",
+        CommandObjectSP(new CommandObjectFrameProviderClear(interpreter)));
+  }
+
+  ~CommandObjectFrameProvider() override = default;
+};
+
 #pragma mark CommandObjectMultiwordFrame
 
 // CommandObjectMultiwordFrame
@@ -1243,6 +1336,8 @@ CommandObjectMultiwordFrame::CommandObjectMultiwordFrame(
   LoadSubCommand("variable",
                  CommandObjectSP(new CommandObjectFrameVariable(interpreter)));
 #if LLDB_ENABLE_PYTHON
+  LoadSubCommand("provider", CommandObjectSP(new CommandObjectFrameProvider(
+                                   interpreter)));
   LoadSubCommand("recognizer", CommandObjectSP(new CommandObjectFrameRecognizer(
                                    interpreter)));
 #endif
diff --git a/lldb/source/Commands/CommandObjectThread.cpp b/lldb/source/Commands/CommandObjectThread.cpp
index bbec714642ec9..0092151a13dd8 100644
--- a/lldb/source/Commands/CommandObjectThread.cpp
+++ b/lldb/source/Commands/CommandObjectThread.cpp
@@ -35,6 +35,7 @@
 #include "lldb/Target/ThreadPlanStepInRange.h"
 #include "lldb/Target/Trace.h"
 #include "lldb/Target/TraceDumper.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/Utility/State.h"
 #include "lldb/ValueObject/ValueObject.h"
 
diff --git a/lldb/source/Interpreter/CMakeLists.txt b/lldb/source/Interpreter/CMakeLists.txt
index 8af7373702c38..ab877ddeecba8 100644
--- a/lldb/source/Interpreter/CMakeLists.txt
+++ b/lldb/source/Interpreter/CMakeLists.txt
@@ -53,6 +53,7 @@ add_lldb_library(lldbInterpreter NO_PLUGIN_DEPENDENCIES
   OptionGroupWatchpoint.cpp
   Options.cpp
   Property.cpp
+  ScriptedFrameProvider.cpp
   ScriptInterpreter.cpp
 
   ADDITIONAL_HEADER_DIRS
diff --git a/lldb/source/Interpreter/ScriptedFrameProvider.cpp b/lldb/source/Interpreter/ScriptedFrameProvider.cpp
new file mode 100644
index 0000000000000..b35ed5d20f6ba
--- /dev/null
+++ b/lldb/source/Interpreter/ScriptedFrameProvider.cpp
@@ -0,0 +1,86 @@
+//===-- ScriptedFrameProvider.cpp ----------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Interpreter/ScriptedFrameProvider.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Thread.h"
+#include "lldb/Utility/ScriptedMetadata.h"
+#include "lldb/Utility/Status.h"
+#include "llvm/Support/Error.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+ScriptedFrameProvider::ScriptedFrameProvider(
+    ThreadSP thread_sp, const ScriptedMetadata &scripted_metadata,
+    Status &error)
+    : m_thread_sp(thread_sp), m_interface_sp(nullptr) {
+  if (!m_thread_sp) {
+    error = Status::FromErrorString(
+        "cannot create scripted frame provider: Invalid thread");
+    return;
+  }
+
+  ProcessSP process_sp = m_thread_sp->GetProcess();
+  if (!process_sp) {
+    error = Status::FromErrorString(
+        "cannot create scripted frame provider: Invalid process");
+    return;
+  }
+
+  ScriptInterpreter *script_interp =
+      process_sp->GetTarget().GetDebugger().GetScriptInterpreter();
+  if (!script_interp) {
+    error = Status::FromErrorString("cannot create scripted frame provider: No "
+                                    "script interpreter installed");
+    return;
+  }
+
+  m_interface_sp = script_interp->CreateScriptedFrameProviderInterface();
+  if (!m_interface_sp) {
+    error = Status::FromErrorString(
+        "cannot create scripted frame provider: Script interpreter couldn't "
+        "create Scripted Frame Provider Interface");
+    return;
+  }
+
+  auto obj_or_err = m_interface_sp->CreatePluginObject(
+      scripted_metadata.GetClassName(), m_thread_sp,
+      scripted_metadata.GetArgsSP());
+  if (!obj_or_err) {
+    error = Status::FromError(obj_or_err.takeError());
+    return;
+  }
+
+  StructuredData::ObjectSP object_sp = *obj_or_err;
+  if (!object_sp || !object_sp->IsValid()) {
+    error = Status::FromErrorString(
+        "cannot create scripted frame provider: Failed to create valid script "
+        "object");
+    return;
+  }
+
+  error.Clear();
+}
+
+ScriptedFrameProvider::~ScriptedFrameProvider() = default;
+
+llvm::Expected<StackFrameListSP> ScriptedFrameProvider::GetStackFrames() {
+  if (!m_interface_sp)
+    return llvm::createStringError(
+        "cannot get stack frames: Scripted frame provider not initialized");
+
+  auto frames = m_interface_sp->GetStackFrames();
+
+  // TODO: Convert StructuredData::ArraySP to StackFrameListSP
+  // This is a placeholder for now
+  return nullptr;
+}
\ No newline at end of file
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
index 09103573b89c5..50569cdefaafa 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
@@ -23,6 +23,7 @@ add_lldb_library(lldbPluginScriptInterpreterPythonInterfaces PLUGIN
   OperatingSystemPythonInterface.cpp
   ScriptInterpreterPythonInterfaces.cpp
   ScriptedFramePythonInterface.cpp
+  ScriptedFrameProviderPythonInterface.cpp
   ScriptedPlatformPythonInterface.cpp
   ScriptedProcessPythonInterface.cpp
   ScriptedPythonInterface.cpp
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 3814f46615078..0b9c7eb107bf5 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -18,6 +18,7 @@
 #include "OperatingSystemPythonInterface.h"
 #include "ScriptedBreakpointPythonInterface.h"
 #include "ScriptedFramePythonInterface.h"
+#include "ScriptedFrameProviderPythonInterface.h"
 #include "ScriptedPlatformPythonInterface.h"
 #include "ScriptedProcessPythonInterface.h"
 #include "ScriptedStopHookPythonInterface.h"
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
new file mode 100644
index 0000000000000..b9a659c44e6f1
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
@@ -0,0 +1,57 @@
+//===-- ScriptedFrameProviderPythonInterface.cpp -------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Host/Config.h"
+#include "lldb/Target/Thread.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/lldb-enumerations.h"
+
+#if LLDB_ENABLE_PYTHON
+
+// LLDB Python header must be included first
+#include "../lldb-python.h"
+
+#include "../SWIGPythonBridge.h"
+#include "../ScriptInterpreterPythonImpl.h"
+#include "ScriptedFrameProviderPythonInterface.h"
+#include <optional>
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::python;
+using Locker = ScriptInterpreterPythonImpl::Locker;
+
+ScriptedFrameProviderPythonInterface::Scripte...
[truncated]

Copy link

github-actions bot commented Oct 3, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@medismailben medismailben force-pushed the scripted-frame-provider branch 3 times, most recently from 89e7ab6 to 9b9d08e Compare October 4, 2025 13:08
@medismailben medismailben changed the title [lldb] Introduce ScriptedFrameProvider [lldb] Introduce ScriptedFrameProvider for real threads Oct 4, 2025
@medismailben medismailben force-pushed the scripted-frame-provider branch 2 times, most recently from 5fd3ebc to 70d72a2 Compare October 4, 2025 14:37
Copy link
Member

@vogelsgesang vogelsgesang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being able to modify the frame list from a script would indeed be amazing - thanks for looking into this! 🙂

As it happens, I am currently looking into gdb's "frame filter" API, which serves a very similar use case - and I am wondering if we should take some inspiration from the gdb APIs here. Gdb's frame filter API differs from the API in this PR in the following points:

  • simpler API: gdb's API only has a single function filter(frame_iter) -> frame_iter function. (This PR currently introduces two functions: get_merge_strategy and get_stack_frames)
  • lazy stack unwinding: afaict, your patch leads to eager materialization of all stack frames. Given that stacks can be pretty deep, it would be preferable to only create those stack frames lazily, as the users inspects the stack. gdb achieves this by only advancing the returned iterator as needed
  • more flexible merging strategies: in gdb, I can use all of Python's iterator support (generator expressions, yield, ...)
  • global registration: in gdb, frame filters are registered globally for all threads. In this PR, the frame providers are registered for each thread individually
  • multiple frame filters: gdb supports registering multiple frame filters at the same time. I didn't use that mechanism, yet, but afaict they are simply chained

The relevant pieces of gdb's documentation:

My motivation for using frame filters in gdb are C++ couroutines. I want to add frames for asynchronous operations. For more background see LLVM's documentatin on Async Stack Traces, in particular the coro bt example. At least for my use case, gdb's design decisions (global registration, flexible merging strategy, ...) are a pretty good fit. But not sure, maybe your current design is a better fit for your particular use case? Which use case are you envisioning?

By the way: would discourse be better-suited for discussing the best approach here? Happy to copy my reply over to discourse, if you would start a thread


// Add real frames after scripted frames (shifted indices)
for (uint32_t i = 0; i < num_real; i++) {
StackFrameSP real_frame = real_frames_sp->GetFrameAtIndex(i);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaict, this materializes the complete backtrace?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only if the user picks the eScriptedFrameProviderMergeStrategyPrepend or eScriptedFrameProviderMergeStrategyAppend merge strategies which is necessary.
You probably picked that up by looking at the other files that the default option is eScriptedFrameProviderMergeStrategyReplace

Copy link
Member

@vogelsgesang vogelsgesang Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

independent of the chosen merge strategy, the get_stackframes Python function always has to return all frames it wants to add.

E.g., for C++ coroutines I would hence have to walk the complete stack to see if any of the stack frames contains a local __coro_frame / __promise variable (the presence of those variables indicatse that additional artificial frames should be injected).

I guess the same would be true for the CPython stack frame provider? Or would that work fundamentally differently?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still working on the CPython Frame Provider but the idea is to iterate over all the frames in the current thread and save the the frame indices for frames coming from the CPython module and since the script would be implemented from python, I was going to use some inspect API to unwind the stack and replace the CPython frames by the python interpreter ones.

uint32_t num_real = real_frames_sp->GetNumFrames(true);
uint32_t num_scripted = scripted_frames_sp->GetNumFrames(false);

switch (strategy) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we leave the merging up to the ScriptedFrameProvider? E.g., by having a get_frames(iterator) -> iterator method similar to gdb's frame filters?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the ScriptedFrameProvider should be responsible for that since the main source of truth should be the Thread itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main source of truth should be the Thread itself

Not sure I can follow. Can you elaborate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ScriptedFrameProvider is just another source of stack frames and hence just an implementation detail.

I don't think it should be in the business of handling the merge strategy and produce the final StackFrameList held by the thread. I believe the thread should asks all its frame providers for all the stack frames available and handle the stack frame list creation itself.

@medismailben
Copy link
Member Author

Hello! Thanks for your interest in this PR.

Being able to modify the frame list from a script would indeed be amazing - thanks for looking into this! 🙂

As it happens, I am currently looking into gdb's "frame filter" API, which serves a very similar use case - and I am wondering if we should take some inspiration from the gdb APIs here. Gdb's frame filter API differs from the API in this PR in the following points:

A non-goal for this PR and for lldb in general is copy what gdb or other debuggers do.

  • simpler API: gdb's API only has a single function filter(frame_iter) -> frame_iter function. (This PR currently introduces two functions: get_merge_strategy and get_stack_frames)
  • lazy stack unwinding: afaict, your patch leads to eager materialization of all stack frames. Given that stacks can be pretty deep, it would be preferable to only create those stack frames lazily, as the users inspects the stack. gdb achieves this by only advancing the returned iterator as needed
  • more flexible merging strategies: in gdb, I can use all of Python's iterator support (generator expressions, yield, ...)
  • global registration: in gdb, frame filters are registered globally for all threads. In this PR, the frame providers are registered for each thread individually
  • multiple frame filters: gdb supports registering multiple frame filters at the same time. I didn't use that mechanism, yet, but afaict they are simply chained

The relevant pieces of gdb's documentation:

My motivation for using frame filters in gdb are C++ couroutines. I want to add frames for asynchronous operations. For more background see LLVM's documentatin on [Async Stack Traces]f(https://clang.llvm.org/docs/DebuggingCoroutines.html#async-stack-traces), in particular the coro bt example. At least for my use case, gdb's design decisions (global registration, flexible merging strategy, ...) are a pretty good fit. But not sure, maybe your current design is a better fit for your particular use case? Which use case are you envisioning?

That sounds pretty exciting. I think ScriptedFrameProvider would be a great fit to support coroutines as long as the ABI doesn't spawn new threads for that. My current use case for this is to translate CPython frames into something that's more understandable, to hopefully improve debugging python / C++ interoperability in lldb.

By the way: would discourse be better-suited for discussing the best approach here? Happy to copy my reply over to discourse, if you would start a thread

This patch introduces a new scripting affordance: `ScriptedFrameProvider`.

This allows users to provide custom stack frames for real native threads,
augmenting or replacing the standard unwinding mechanism. This is useful
for:
- Providing frames for custom calling conventions or languages
- Reconstructing missing frames from crash dumps or core files
- Adding diagnostic or synthetic frames for debugging

The frame provider supports four merge strategies:
- Replace: Replace entire stack with scripted frames
- Prepend: Add scripted frames before real stack
- Append: Add scripted frames after real stack
- ReplaceByIndex: Replace specific frames by index

With this change, frames can be synthesized from different sources:
- Either from a dictionary containing a PC address and frame index
- Or by creating a ScriptedFrame python object for full control

To use it, first register the scripted frame provider then use existing
commands:
   (lldb) frame provider register -C my_module.MyFrameProvider

      or

   (lldb) script thread.RegisterFrameProvider("my_module.MyFrameProvider", lldb.SBStructuredData())

      then

   (lldb) bt

See examples/python/templates/scripted_frame_provider.py for details.

Architecture changes:
- Moved ScriptedFrame from `Plugins` to `Interpreter` to avoid
   layering violations
- Moved `RegisterContextMemory` from `Plugins` to `Target` as it only
   depends on Target and Utility layers
- Added `ScriptedFrameProvider` C++ wrapper and Python interface
- Updated `Thread::GetStackFrameList` to apply merge strategies

rdar://161834688

Signed-off-by: Med Ismail Bennani <[email protected]>
@JDevlieghere
Copy link
Member

A non-goal for this PR and for lldb in general is copy what gdb or other debuggers do.

While compatibility is not generally a goal, there's also no reason to diverge for the sake of it. However, what I think Adrian is suggesting here, is that we can benefit from building on top of the learnings of an existing implementation. I think he raises some good points that are worth evaluating in the context of LLDB.

@medismailben medismailben force-pushed the scripted-frame-provider branch from 70d72a2 to ab4a9b8 Compare October 6, 2025 17:00
@vogelsgesang
Copy link
Member

vogelsgesang commented Oct 6, 2025

A non-goal for this PR and for lldb in general is copy what gdb or other debuggers do.

While compatibility is not generally a goal, there's also no reason to diverge for the sake of it. However, what I think Adrian is suggesting here, is that we can benefit from building on top of the learnings of an existing implementation.

yes. Thanks for summing it up!

Use cases

My (@vogelsgesang's) motivation for using frame filters in gdb are C++ coroutines.

I polished my existing script and now posted it in PR #162145. The most interesting piece is CppCoroutineFrameFilter. With this filter, I get the backtrace

(gdb) bt
#0  write_output(std::basic_string_view<char, std::char_traits<char> >) [clone .resume] (contents=...) at async-task-example.cpp:6
[async] greet () at async-task-example.cpp:12
[async] std::__n4861::coroutine_handle<std::__n4861::noop_coroutine_promise>::__frame::__dummy_resume_destroy() at <...>
#1  0x000055555555a0f8 in std::__n4861::coroutine_handle<task::promise_type>::resume (this=0x7fffffffd5a8) at /usr/include/c++/14/coroutine:242
#2  0x0000555555557bb9 in task::syncStart (this=0x7fffffffd5a8) at async-task-library.hpp:78
#3  0x00005555555552de in main () at async-task-example.cpp:18

The two lines prefixed with [async] are the additional frames injected by the yield from _create_coroutine_frames(parent_coro, inferior_frame) in the frame filter. (The C++ source code can also be found in the another section of the same guide, in case anyone wants to reproduce this example)

My (@medismailben's) current use case for this is to translate CPython frames into something that's more understandable, to hopefully improve debugging python / C++ interoperability in lldb.

Could you provide an example of a ScriptedFrameProvider implementation for pretty-printing the CPython frames? Or at least the intended bt output which you are envisioning for an example Python program?

I am currently slightly struggling with imagining the intended usage of the proposed APIs, and such an example might help me. (Of course only if possible and not blocked, e.g., due to intellectual property issues)

# Attach a frame provider to a thread
thread = process.GetSelectedThread()
error = lldb.SBError()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error seems unused?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, it's actually the return value of SBThread::SetScriptedFrameProvider. I'll update the doc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants